iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Software Development

1+1+1>3 ~ Spark 與 DataFusion、Comet 效能煉金術 ~系列 第 20

Day 20 Iceberg 怎麼接上引擎:TableProvider、FileScanTask

  • 分享至 

  • xImage
  •  

D19 最後提到 pushdown 能不能生效,要回到 TableProvider 來看,今天就來說明這一段,順便看 Iceberg 表格式版本的演進。

今天回答三個問題:

  • 表格式要湊齊什麼,引擎才會把它當資料源?
  • V1 到 V3 各解決了什麼痛點?
  • 讀不懂新版表的時候會發生什麼事?

檔案格式 vs 表格式

Parquet 是檔案格式,管一份檔案內部長怎樣。Iceberg 是表格式,管一堆檔案怎麼組成一張表、怎麼演進、怎麼刪、怎麼快照。

引擎不直接讀 Parquet,而是先問表格式:這張表有哪些檔?這幾個 predicate 你接不接?有沒有 snapshot 可以回到上週?

這個「問」的介面就是契約。DataFusion 叫 TableProvider,Spark DSv2 叫 Table + ScanBuilder,名字不同,要回答的問題一樣。

六項最小能力

能力 引擎會問什麼 沒這項會怎樣
分區資訊 能不能只讀符合條件的 partition? scan 只能全掃
下推支援 這幾個 predicate / projection 你接不接? filter pushdown、column pruning 退化到 executor 層
併發資訊 可以幾個 task 平行讀? 併行度只能靠檔案數猜
排序資訊 資料本來就排好了嗎? SortMergeJoin 無法沿用天然順序
統計資訊 min / max / null 分布、row count 有嗎? optimizer 只能猜 selectivity 跟 join order
更新資訊 表格式 V 幾?有沒有 delete file? 讀出來的資料混進「應該被刪」的列

少任何一項,optimizer 的決策鏈就斷一節。

FileScanTask

這六項都要在規劃階段決定,執行階段太晚了。Iceberg 規劃完的產物叫 FileScanTask

FileScanTask #1
  data file:          s3://.../part-00042.parquet
  partition:          date=2026-08-25
  residual predicate: amount > 100
  delete files:       [dv-00042.puffin]

重點是自足:executor 拿到它就能開工,不用再問 catalog、不用再打開 metadata。這個性質等一下講 Comet 時會直接用到。

V1 到 V3

每一版都是在補前一版擋住的東西。

V1(2018):先把「一堆檔案」變成「一張表」

Hive 的做法是把分區值寫進資料夾路徑,/sales/dt=2026-08-25/。三個問題:

  • 查詢必須猜到路徑長怎樣。你寫 WHERE ts > '2026-08-25',Hive 不知道那對應到 dt 這個資料夾,剪不掉。
  • 想換分區方式(從按天改成按小時)等於整張表重排。
  • 要知道有哪些檔案得去 list 目錄,在物件儲存上這件事又慢又貴,而且沒有原子性的 commit。

V1 的解法是用 metadata 描述檔案,不用目錄。表的狀態是一棵樹:metadata.json 指向 snapshot,snapshot 指向 manifest list,manifest list 指向一堆 manifest,manifest 裡才列出資料檔以及每個檔案的分區值與欄位統計。

這棵樹一換,三件事同時解決:

  • Hidden partitioning:分區值是從欄位透過 transform(day(ts))算出來、記在 metadata 裡的。使用者照常寫 WHERE ts > ...,剪枝由 Iceberg 自己做,不用背路徑規則。
  • Partition spec 可演進:改分區方式只是新增一個 spec,舊檔案沿用舊 spec,兩邊同時可讀,不用重寫。
  • Snapshot:每次 commit 產生一個不可變的 snapshot,換 metadata.json 的指標就是原子操作。時光回溯順便免費拿到。

要注意 V1 不是只支援 append,它也能 overwrite,只是變更的最小單位是「整個檔案」。

V2(2021):讓刪改不用重寫整檔

V1 那個「最小單位是整檔」就是痛點。一張 500 MB 的檔案裡刪掉一列,得把整份讀出來、濾掉那列、重寫一份新的,這就是 D12 講的 copy-on-write。合規刪除、CDC upsert 這類「改動很小但很頻繁」的場景會被打爆。

V2 的解法是另外寫一種檔案,專門記「哪些列不算數」,讀的時候再合併掉,也就是 merge-on-read。兩種形式:

  • positional delete:記「哪個檔案的第幾列」,精準但要先知道位置。
  • equality delete:記「欄位值等於這些的都刪掉」,寫的時候不用先讀舊資料,適合串流 upsert,代價是讀端要多做比對。

但這裡冒出一個新問題:資料檔跟 delete file 都在長,讀的時候怎麼知道哪個 delete 該套用到哪個資料檔?V2 的答案是 sequence number。每次 commit 拿到一個遞增序號,delete file 只作用在序號比它舊的資料檔上。沒有這個序號,MOR 的結果就沒有明確定義。

V3(2025 定案):delete file 自己變成問題

V2 用了幾年之後,delete file 本身成了瓶頸。每次刪除都新增一份 delete file,它們會一直堆積,讀一個資料檔可能要先打開十幾份 delete file 再做合併。D12 已經拆過這筆成本。

V3 的主力是 deletion vector:一個資料檔對應一份 bitmap(Roaring bitmap,存在 Puffin 檔裡),記錄哪些列被刪。關鍵不在格式而在數量,新的刪除是改寫那份 bitmap,不是再多疊一份檔案。所以讀成本從「跟刪除次數成長」變成固定的一份。它取代的是 positional delete,equality delete 在 V3 仍然保留。

其他幾項:

  • Row lineage:v3 之後的表必須替每一列追蹤 _row_id_last_updated_sequence_number。有了它,找出兩個 snapshot 之間改了哪些列變成一次查詢,而不是一次全表 diff。CDC 真正變便宜是在這裡,不是 V2。 V2 只是讓 CDC 可行。
  • VARIANT:半結構化資料的原生型別,不用再把 JSON 塞進 string 欄位。
  • geometry / geography:地理型別。
  • 表層加密:data、delete、manifest、manifest list 都加密並防篡改(metadata.json 不含資料與統計,不在範圍內)。
  • 還有 default value、奈秒 timestamp。

時間軸別寫太早。 規格 2025 年才定案,引擎落地拖到 2026:AWS 2025 年 11 月、Snowflake 2026 年 5 月 GA、Databricks 2026 年 4 月 Preview,開源 Trino 至今未支援。而且 V4 已經在開發中了。

一句話串起來

V1 把目錄換成 metadata,V2 把「整檔重寫」換成 delete file,V3 把「delete file 堆積」換成單一 bitmap。每一版都是把上一版的解法所產生的新成本再收掉一次。

讀不懂新版表會怎樣

直覺會覺得舊 reader 應該「看得懂多少讀多少」。規格說的正好相反。

Iceberg spec 對 format version 的定義是:版本號遞增就是因為新功能會破壞 forward compatibility,舊 reader 沒辦法正確讀出新表。所以 V1 reader 拿到 V2 表,正確行為是看 format-version,超出支援範圍就直接拒絕開表

這是 fail closed,不是降級。「保守一點、不讀 delete file」那條路不存在,因為忽略 delete file 等於把刪掉的列讀回來,那正是最典型的錯誤結果。

想讓舊引擎繼續讀,規格給的答案是寫端不要升版。旋鈕在寫端,不在讀端。

(順帶一提,Delta 走的是另一條路:readerFeatures / writerFeatures 兩張清單,client 逐項比對。Iceberg 的 format-version 是單一、全有全無的閘門,沒有這種細粒度。)

那優雅降級在哪一層

實作層

層級 機制 粒度 不支援時
規格層 Iceberg format-version 全表 拒絕開表
實作層 Comet fallback 逐功能、逐 operator 退回 Spark 執行,結果仍正確

差別在有沒有保底實作可以退。Comet 敢降級,是因為旁邊站著語義完整的 Spark 加 Iceberg Java;Iceberg 規格層沒有這個保底,所以只能拒絕。

Comet 1.0 剛好是範例:支援 Iceberg 1.11,落地第一個 V3 功能「表層加密」,deletion vector 跟 VARIANT 則 fallback。

這裡有個容易做錯的歸因:這不是規格決定的,是實作成熟度決定的。 加密先落地,是因為 Parquet Modular Encryption 早就在 Comet 已經在用的 arrow-rs 裡;deletion vector 還沒接手,是因為 Puffin 加 Roaring bitmap 的解讀在 Rust 側還在做。規格對兩者一視同仁。

iceberg-rust:把邊界畫在 FileScanTask

comet 攤開的是兩套已經在跑的實作

  1. 整合 Iceberg Java:JVM 功能好接,但要 patch Iceberg 原始碼、靠 shading 繞版本衝突,還跟 Iceberg 專案形成循環依賴。
  2. 整合 iceberg-rust:未修改的 Iceberg Java 負責規劃(catalog、partition pruning),Comet 把 FileScanTask 序列化給 iceberg-rust 原生執行。

社群標準化在第二條,到 Comet 1.0,切分很明確:規劃留在 driver 的 Iceberg Java,讀取移到 executor 的 iceberg-rust。

Rust 側只要一個 FileScanTask 就能自足執行,完全不需要理解「Iceberg 是什麼」。這跟 D15 講的「Comet 替換邊界在 physical plan 之後」是同一條原則,只是這次邊界更精細,落在 scan planning 之後。

代價要講清楚:規劃開銷原封不動留在 JVM 側。省下來的是掃描本身的成本,不是規劃成本。

結論

六項能力是引擎跟表格式的最小契約面。Iceberg 一路加功能,但相容性策略是寧可讀不到,也不要讀錯format-version 是單一閘門,支援就開、不支援就拒絕。真正的優雅降級發生在實作層,而哪個功能能走 native,是實作成熟度說了算。

明天預告

D21 進 Comet 內部第一站,回到 D15 六接點的第一個,plan 攔截:physical plan 什麼時候被換掉?AQE 執行期重規劃時,攔截規則怎麼保持一致?

那就明天見~

參考資料


上一篇
Day 19 SortExec 記憶體不夠時怎麼寫磁碟:排序、Pushdown與 Top-K
下一篇
Day 21 Apache Comet 表達式:native、codegen dispatcher 與 fallback
系列文
1+1+1>3 ~ Spark 與 DataFusion、Comet 效能煉金術 ~21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言